텍스트 분류는 "이 영화 진짜 재밌다!" 라는 문장을 '긍정'으로,
"시간 아까웠음" 이라는 문장을 '부정'으로 컴퓨터가 알아서 분류하게 만드는 것입니다.
이 목표를 위해 딥러닝 모델이 어떤 사고의 과정을 거치는지 단계별로 따라가 보겠습니다.
컴퓨터는 '재미있다'라는 단어의 의미를 모릅니다. 오직 숫자만 이해할 뿐이죠.
그래서 모든 자연어 처리의 첫 번째 관문은 "단어를 어떻게 숫자로 바꿀 것인가?" 입니다.
가장 단순한 방법은 세상의 모든 단어에 번호를 매기고,
해당 단어의 번호만 1로 표시하고 나머지는 모두 0으로 채우는 겁니다.
단어장이 [나, 는, 영화, 본다] 라면, '영화'는 [0, 0, 1, 0] 이 됩니다.
하지만 이 방법엔 치명적인 단점이 있습니다.
1.차원의 저주
단어가 10,000개면 10,000차원 벡터가 필요해요. 너무 비효율적입니다.
2. 의미 실종
'영화' [0,0,1,0] 와 '영사기' [...1...] 는 의미적으로 관련이 깊지만,
원-핫 벡터 상에서는 아무런 관계가 없는, 그냥 서로 다른 숫자일 뿐입니다.
이 문제를 해결하기 위해 "단어의 의미를 다차원 공간의 좌표" 로 표현하는 '임베딩'이 등장합니다.
'왕' 이라는 단어는 (0.8, 0.2, 0.9) '여왕' 이라는 단어는 (0.7, 0.9, 0.8) '사과' 라는 단어는 (-0.5, 0.6, -0.4)
이런 식으로 표현하는 거죠. 이렇게 하면 놀라운 일이 벌어집니다.
의미가 비슷한 단어들은 공간상에서 가까운 곳에 위치하게 돼요.
이제 모델은 단어 간의 관계(예: 왕 - 남자 + 여자 = 여왕)를 벡터 연산을 통해 학습할 수 있게 됩니다.
텍스트를 처리하기 위해, 우리는 의미가 없는 원-핫 벡터 대신, 단어의 의미와 관계를 함축한 저차원의 '임베딩 벡터'를 사용합니다. 이것이 바로 모델의 첫 번째 층인 임베딩 레이어(Embedding Layer)의 역할입니다.
"나는 너를 사랑해"와 "너는 나를 사랑해"는 같은 단어로 이루어져 있지만 의미가 다릅니다.
바로 '순서' 때문입니다. RNN(Recurrent Neural Network) 은 이 '순서'와 '문맥'을 파악하는 아키텍쳐입니다.
RNN은 단어 임베딩 벡터를 하나씩 순서대로 입력받습니다.
그리고 각 단어를 볼 때마다 내부적으로 '기억' (Hidden State) 을 계속 업데이트해요.
"나는" 이라는 단어를 보고 기억을 업데이트 👉 기억 ver 1.0 "너를" 이라는 단어를 보고, 기억 ver 1.0을 참고하여 기억을 업데이트 👉 기억 ver 2.0 "사랑해" 라는 단어를 보고, 기억 ver 2.0을 참고하여 기억을 업데이트 👉 기억 ver 3.0 (최종)
이 최종 기억(기억 ver 3.0)에는 문장 전체의 정보가 압축되어 담겨있다고 보는 겁니다.
RNN은 순차적인 데이터(문장)를 처리하며 문맥 정보를 '은닉 상태(Hidden State)'에 압축합니다.
특히 양방향, 다층 구조를 통해 훨씬 더 정교하게 문장의 의미를 파악할 수 있습니다.
이제 모든 재료가 준비되었습니다. 모델이 최종적으로 "이 문장은 '긍정'이야!" 라고 판단하는 전체 과정을 정리해 봅시다.
입력 : "이 영화 진짜 재밌다" 라는 문장이 들어옵니다. (정확히는 단어 번호의 배열)
1. 임베딩
각 단어("이", "영화", "진짜", "재밌다")가 의미를 담은 임베딩 벡터로 변환됩니다.
2. RNN 처리
임베딩 벡터들이 순서대로 양방향, 다층 RNN에 입력됩니다.
RNN은 문장을 앞에서부터, 그리고 뒤에서부터 꼼꼼히 읽으며 문맥 전체를 압축한 최종 정보 벡터를 만들어냅니다.
3. 정보 선택 (Slicing)
RNN은 사실 모든 단어를 읽을 때마다 중간 결과물을 내놓습니다.
하지만 우리는 문장 전체를 요약한 정보가 필요하므로, 가장 마지막 단어까지 읽었을 때의 최종 결과물만 딱 잘라내서 사용합니다.
4. 분류 (Softmax)
이 최종 정보 벡터를 Softmax라는 분류기에 넣습니다.
Softmax는 이 벡터를 보고 '긍정'일 확률 95%, '부정'일 확률 5% 와 같이 각 클래스별 확률 값으로 변환해 줍니다.
결론 : 가장 확률이 높은 '긍정'을 최종 예측 결과로 내놓습니다.
이론적으로는 완벽해 보이지만, 실제 데이터를 다룰 땐 한 가지 귀찮은 문제가 있었습니다.
바로 "문장마다 길이가 다르다"는 점입니다. 모델은 고정된 크기의 묶음(미니배치)을 처리해야 하는데, 길이가 제각각인 문장들을 어떻게 하나의 묶음으로 만들 수 있을까요?
정답은 "가장 긴 문장을 기준으로 짧은 문장의 뒤에 의미 없는 숫자(예: 0)를 붙여 길이를 맞추는 것", 즉 패딩(Padding) 입니다. 이때, 데이터 로더(DataLoader)가 미니배치를 구성할 때 이 패딩 작업을 동적으로 처리해주는 아주 똑똑한 일꾼이 바로 콜레이트 함수(Collate Function) 입니다.
DataLoader는 Dataset에서 문장들을 하나씩 가져와 리스트 [문장1, 문장2, 문장3] 를 만듭니다.
이 리스트를 collate_fn에게 넘겨주면, collate_fn은 이 리스트 안에서 가장 긴 문장을 찾아, 나머지 짧은 문장들 뒤에 패딩을 붙여 모두 같은 길이로 만든 뒤, 하나의 커다란 텐서 덩어리로 합쳐서 모델에게 전달합니다. 덕분에 우리는 문장 길이에 신경 쓰지 않고 효율적으로 모델을 학습시킬 수 있게 됩니다.
가변 길이의 문장들을 묶어 처리하기 위해 패딩(Padding) 이라는 기법을 사용하며, 이 작업은 DataLoader 내부의 collate_fn이 각 미니배치마다 동적으로 수행해 줍니다.